<template>
  <div class="page-line-example">
    <h1>page 分页示例</h1>

    <p class="hint">
      schema 仅包含 document → paragraph → text。编辑区负责内容编辑，渲染区会把段落按行拆分并根据页面宽高分页。
      页面 (`page`) 与行 (`line`) 节点仅存在于渲染阶段，不会写入最终文档 JSON。
    </p>

    <div class="workspace">
      <div class="editor-pane">
        <h2>编辑区</h2>
        <div class="editor-shell" v-if="editor">
          <EditorContent :editor="editor" class="editor-surface" />
        </div>
        <div class="actions">
          <button @click="logJson">打印文档 JSON</button>
          <button @click="logHtml">打印文档 HTML</button>
          <button @click="clearOutput" :disabled="!output">清空输出</button>
        </div>
      </div>

      <div class="preview-pane">
        <h2>分页渲染</h2>
        <div class="page-preview">
          <div
            v-for="(page, pageIndex) in pages"
            :key="page.id"
            class="page"
          >
            <div class="page-inner">
              <div
                v-for="line in page.lines"
                :key="line.id"
                class="line"
                :class="{
                  'line--gap': line.isGap,
                  'line--empty': !line.text && !line.isGap
                }"
                :style="line.isGap ? { height: `${line.height}px` } : undefined"
              >
                <span
                  v-if="!line.isGap"
                  :style="line.isFirstInParagraph && line.text
                    ? { textIndent: `${metrics.firstLineIndentEm}em` }
                    : undefined"
                >{{ line.text }}</span>
              </div>
            </div>
            <footer class="page-footer">Page {{ pageIndex + 1 }}</footer>
          </div>

          <p v-if="!pages.length" class="empty">暂无内容</p>
        </div>
      </div>
    </div>

    <pre class="output" v-if="output">{{ output }}</pre>

    <div ref="measureHost" class="page-measure" aria-hidden="true"></div>
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { doc } from './doc.js'

const PlainDocument = Document.extend({
  content: 'paragraph+'
})

export default {
  name: 'RenderView',
  components: {
    EditorContent
  },
  data() {
    return {
      editor: null,
      output: '',
      pages: [],
      pageSize: {
        width: 620,
        height: 820
      },
      pagePadding: {
        horizontal: 48,
        top: 40,
        bottom: 56
      },
      metrics: {
        fontSize: 14,
        lineHeight: 22.4,
        fontFamily: '"PingFang SC", "Microsoft YaHei", Arial, sans-serif',
        paragraphSpacing: 12,
        firstLineIndentEm: 2
      },
      measureCtx: null
    }
  },
  mounted() {
    this.editor = new Editor({
      extensions: [PlainDocument, Paragraph, Text],
      content: doc,
      // content: {
      //   type: 'doc',
      //   content: [
      //     {
      //       type: 'paragraph',
      //       content: [
      //         {
      //           type: 'text',
      //           text: '这是一个只包含 paragraph 和 text 节点的简单示例。渲染区的 page 与 line 节点来自前端拆分，不参与文档存储。'
      //         }
      //       ]
      //     }
      //   ]
      // }
    })

    this.editor.on('update', this.updatePages)
    this.ensureMeasureContext()
    this.$nextTick(this.updatePages)
  },
  beforeDestroy() {
    if (this.editor) {
      this.editor.off('update', this.updatePages)
      this.editor.destroy()
    }
    if (this.$refs.measureHost) {
      this.$refs.measureHost.innerHTML = ''
    }
    this.measureCtx = null
  },
  methods: {
    ensureMeasureContext() {
      if (this.measureCtx) {
        return this.measureCtx
      }

      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      if (!ctx) {
        return null
      }

      ctx.font = `${this.metrics.fontSize}px ${this.metrics.fontFamily}`
      this.measureCtx = ctx
      return ctx
    },
    breakTextIntoLines(text, maxWidth, firstLineIndentPx = 0) {
      const ctx = this.ensureMeasureContext()
      if (!ctx) {
        return []
      }

      ctx.font = `${this.metrics.fontSize}px ${this.metrics.fontFamily}`

      const glyphs = Array.from(text)
      const lines = []
      let buffer = ''
      let glyphCount = 0
      let isFirstLine = true
      let firstLineLimit = maxWidth - firstLineIndentPx
      if (firstLineLimit <= 0) {
        firstLineLimit = maxWidth
      }
      let currentLimit = isFirstLine ? firstLineLimit : maxWidth

      const pushLine = () => {
        lines.push({ text: buffer, length: glyphCount })
        buffer = ''
        glyphCount = 0
        isFirstLine = false
        currentLimit = maxWidth
      }

      glyphs.forEach(glyph => {
        if (glyph === '\n') {
          pushLine()
          isFirstLine = false
          currentLimit = maxWidth
          return
        }

        const next = buffer + glyph
        const width = ctx.measureText(next).width

        if (width <= currentLimit || !buffer) {
          buffer = next
          glyphCount += 1
        } else {
          pushLine()
          buffer = glyph
          glyphCount = 1
          isFirstLine = false
          currentLimit = maxWidth
        }
      })

      if (buffer || !lines.length) {
        pushLine()
      }

      return lines
    },
    updatePages() {
      if (!this.editor) {
        this.pages = []
        return
      }

      const ctx = this.ensureMeasureContext()
      const measureHost = this.$refs.measureHost
      if (!ctx || !measureHost) {
        this.pages = []
        return
      }

      ctx.font = `${this.metrics.fontSize}px ${this.metrics.fontFamily}`

      const contentWidth = this.pageSize.width - this.pagePadding.horizontal * 2
      const contentHeight = this.pageSize.height - this.pagePadding.top - this.pagePadding.bottom
      const paragraphSpacing = this.metrics.paragraphSpacing
      const firstLineIndentEm = this.metrics.firstLineIndentEm || 0
      const firstLineIndentPx = firstLineIndentEm * this.metrics.fontSize

      const buildMeasurePage = () => {
        measureHost.innerHTML = ''
        const page = measureHost.ownerDocument.createElement('div')
        page.style.width = `${this.pageSize.width}px`
        page.style.height = `${this.pageSize.height}px`
        page.style.padding = `${this.pagePadding.top}px ${this.pagePadding.horizontal}px ${this.pagePadding.bottom}px`
        page.style.boxSizing = 'border-box'
        page.style.display = 'flex'
        page.style.flexDirection = 'column'

        const inner = measureHost.ownerDocument.createElement('div')
        inner.style.flex = '1'
        inner.style.display = 'flex'
        inner.style.flexDirection = 'column'
        inner.style.fontFamily = this.metrics.fontFamily
        inner.style.fontSize = `${this.metrics.fontSize}px`
        inner.style.lineHeight = `${this.metrics.lineHeight}px`
        inner.style.color = '#1f2933'

        page.appendChild(inner)
        measureHost.appendChild(page)
        return inner
      }

      const createLineDom = line => {
        const el = measureHost.ownerDocument.createElement('div')
        el.style.display = 'flex'
        el.style.alignItems = 'center'
        el.style.whiteSpace = 'pre-wrap'
        el.style.wordBreak = 'break-all'
        el.style.width = '100%'

        if (line.isGap) {
          el.style.minHeight = '0'
          el.style.height = `${line.height}px`
          return el
        }

        el.style.minHeight = `${this.metrics.lineHeight}px`
        const span = measureHost.ownerDocument.createElement('span')
        span.textContent = line.text || ''
        span.style.display = 'inline-block'
        span.style.width = '100%'
        span.style.whiteSpace = 'inherit'
        if (line.isFirstInParagraph && line.text) {
          span.style.textIndent = `${firstLineIndentEm}em`
        }
        el.appendChild(span)
        return el
      }

      const docNode = this.editor.state.doc
      const pages = []
      let pageIndexCounter = 0
      let lineCounter = 0
      let paragraphIndex = 0
      let measureInner = buildMeasurePage()
      let currentPage = {
        id: `page-${pageIndexCounter++}`,
        lines: []
      }

      const startNewPage = () => {
        if (currentPage.lines.length) {
          pages.push(currentPage)
        }

        currentPage = {
          id: `page-${pageIndexCounter++}`,
          lines: []
        }
        measureInner = buildMeasurePage()
      }

      docNode.forEach((node, offset, index) => {
        if (node.type.name !== 'paragraph') {
          return
        }

        const paragraphText = node.textContent || ''
        const lineSegments = this.breakTextIntoLines(paragraphText, contentWidth, firstLineIndentPx)
        let consumed = 0

        lineSegments.forEach((segment, lineIndex) => {
          const lineText = segment.text
          const lineLength = segment.length

          const lineData = {
            text: lineText,
            isGap: false,
            isFirstInParagraph: lineIndex === 0,
            paragraphIndex
          }
          let dom = createLineDom(lineData)
          measureInner.appendChild(dom)

          if (measureInner.offsetHeight > contentHeight && currentPage.lines.length) {
            measureInner.removeChild(dom)
            startNewPage()
            dom = createLineDom(lineData)
            measureInner.appendChild(dom)
          }

          currentPage.lines.push({
            text: lineData.text,
            isGap: false,
            isFirstInParagraph: lineData.isFirstInParagraph,
            paragraphIndex,
            id: `line-${++lineCounter}`
          })

          consumed += lineLength
        })

        const isLastParagraph = index === docNode.childCount - 1
        if (!isLastParagraph && paragraphSpacing > 0) {
          const gapLine = {
            text: '',
            isGap: true,
            height: paragraphSpacing
          }

          const gapDom = createLineDom(gapLine)
          measureInner.appendChild(gapDom)

          if (measureInner.offsetHeight > contentHeight && currentPage.lines.length) {
            measureInner.removeChild(gapDom)
            startNewPage()
          } else {
            currentPage.lines.push({
              text: '',
              isGap: true,
              height: gapLine.height,
              id: `line-${++lineCounter}`
            })
          }
        }

        paragraphIndex += 1
      })

      if (currentPage.lines.length) {
        pages.push(currentPage)
      }

      measureHost.innerHTML = ''

      this.pages = pages
    },
    logJson() {
      this.output = JSON.stringify(this.editor.getJSON(), null, 2)
    },
    logHtml() {
      this.output = this.editor.getHTML()
    },
    clearOutput() {
      this.output = ''
    }
  }
}
</script>

<style scoped>
.page-line-example {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.hint {
  margin: 0;
  color: #666;
  font-size: 13px;
}

.workspace {
  display: flex;
  gap: 24px;
  align-items: flex-start;
}

.editor-pane,
.preview-pane {
  flex: 1;
  min-width: 0;
}

.editor-pane h2,
.preview-pane h2 {
  margin: 0 0 12px;
  font-size: 16px;
}

.editor-shell {
  border: 1px solid #d9d9d9;
  border-radius: 6px;
  padding: 12px;
  background: #fff;
  min-height: 300px;
}

.editor-surface {
  min-height: 260px;
}

.actions {
  display: flex;
  gap: 12px;
}

.actions button {
  padding: 6px 12px;
  cursor: pointer;
}

.page-preview {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.page {
  width: 620px;
  height: 820px;
  padding: 40px 48px 56px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}

.page-inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0;
  color: #1f2933;
  font-size: 14px;
  line-height: 1.6;
}

.page-footer {
  text-align: right;
  font-size: 12px;
  color: #94a3b8;
  margin-top: 12px;
}

.line {
  display: flex;
  align-items: center;
  min-height: 22.4px;
  white-space: pre-wrap;
  word-break: break-all;
}

.line span {
  display: inline-block;
  width: 100%;
  white-space: inherit;
}

.line--gap {
  min-height: 0;
}

.line--empty span {
  visibility: hidden;
}

.empty {
  margin: 0;
  color: #999;
}

.page-measure {
  position: absolute;
  left: -9999px;
  top: 0;
  visibility: hidden;
}

.output {
  background: #0f172a;
  color: #e2e8f0;
  padding: 12px;
  border-radius: 6px;
  overflow: auto;
  max-height: 200px;
}
</style>
